package com.hup.winHolderService.ws;

import com.hup.winHolderService.framework.exception.AppServerException;
import com.hup.winHolderService.service.MockActionService;
import com.hup.winHolderService.service.WsService;
import com.hup.winHolderService.ws.handler.WsDataHandler;
import com.hup.winHolderService.ws.handler.parser.WsDataParser;
import com.hup.winHolderService.ws.util.WebSocketUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.Session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Vector;

/**
 * websocket客户端处理类
 * .功能: 心跳管理, 消息处理, 认证等
 * <p>
 * 说明
 * .摘取自BudsDemo, 目的是实现[框架,业务]功能(适配tomcat和spring的websocket)
 * .已移除: 群发数据, clients上下线通知等
 *
 * @author hugan
 * @date 2022/5/18
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WsClientHandler {
    /**
     * 心跳发送间隔,ms
     */
    private static final int BEATS_SPAM = 5000;
    //private static final int BEATS_SPAM = 1000;//test
    /**
     * n次心跳异常时,断开连接
     */
    private static final int IDLE_TIME = 3;
    /**
     * 已连接的客户端
     */
    @Getter
    private final Vector<WsClient> clients = new Vector<>();

    private final WsDataHandler wsDataHandler = new WsDataHandler();

    private final WsService wsService;
    private final MockActionService mockActionService;

    @PostConstruct
    public void init() {
        wsService.setClientHandler(this);
        wsDataHandler.addService(wsService);
        mockActionService.setClientHandler(this);
        wsDataHandler.addService(mockActionService);
        wsDataHandler.addService(this);
        //wsDataHandler.addService(new WsTest());//test
        /*
        简陋的轮询心态检测,
        问题:
        .目前只在pong时更新时间; 更合理的是 在每次收到消息后都更新
        .尽量低仿netty的IdleStateHandler
        .仅示例,没过多测试,不排除有bug
        //
        .不适合客户端数量巨大的情况!! 轮询任务中间会复制clients对象;
         不过 一台物理机/一个程序 的最大承载量 应该不会很巨大???
         */
        new Thread("ws-beats") {
            @Override
            public void run() {
                while (true) {
                    long current = System.currentTimeMillis();
                    ArrayList<WsClient> copy = new ArrayList<>(clients);

                    copy.forEach(client -> {
                        if (!client.getHadConnected()) return;
                        try {
                            client.sendPing();
                        } catch (IOException e) {
                            // 不应马上修改状态,BEATS_SPAM * n来处理/允许断开后重连
                            //client.connected = false;
                        }
                    });
                    copy.forEach(client -> {
                        if (!client.getHadConnected()) return;//收发数据时,明确断开的连接
                        long diff = current - client.getLastPongTime();
                            /*
                            n次没回复心跳,就移除客户端
                            场景: 如wifi断开,是不会马上有socket异常的
                             */
                        if (diff > BEATS_SPAM * IDLE_TIME) {
                            client.setHadConnected(false);
                            log.info("断开心跳异常的连接={}", client.getUser());
                            /*
                            注意!!
                            此处关闭tomcat.session连接时,
                            会在当前线程触发(不是异步,比较奇怪...) WsClientHandler.onClosed, 里面会调用 clients.remove(client);
                            所以要用copy!!
                             */
                            client.doCloseSilent();
                        }
                    });
                    copy.clear();

                    try {
                        Thread.sleep(BEATS_SPAM);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        log.error("", e);
                        return;
                    }
                }
            }
        }.start();
    }

    public void onConnected(WsClient client, Session session) {
        String address = WebSocketUtil.getRemoteAddress(session);
        log.info("onConnected, address={}", address);
        client.setHadConnected(true);
        client.setHadAuth(false);
        client.setLastPongTime(System.currentTimeMillis());
        client.setUser(address);
        clients.add(client);
    }

    public void onClosed(WsClient client, CloseReason reason) {
        //log.info("onClose, {}", reason.getCloseCode() == CloseCodes.NORMAL_CLOSURE ? "正常关闭" : reason);
        if (CloseCodes.TOO_BIG == reason.getCloseCode()) {
            log.warn("连接已关闭: 发送数据超长 user={}", client.getUser());
        }
        client.setHadConnected(false);
        clients.remove(client);

        mockActionService.releaseActionFunction();//只要有用户下线,就释放按键
        wsService.onClientChanged(false, client);
    }

    public void onError(WsClient client, Throwable e) {
        if (e.getMessage().contains("强迫关闭")) {
            /*
            msg=远程主机强迫关闭了一个现有的连接。
            场景:主动断开心跳异常的连接
             */
            return;
        }
        new AppServerException("ws异常:" + client.getUser(), e).doLog();
    }

    /**
     * 收到心跳包
     */
    public void onPongMessage(WsClient client) {
        //log.info("onPongMessage={}", client.getUser());
        if (client.getHadAuth()) {
            //没认证通过前, 都不更新时间; n次心跳还没认证 就会自动断开
            client.setLastPongTime(System.currentTimeMillis());
        }
    }

    public void onMessage(String msg, WsClient client) {
        log.warn("请求异常: 不能直接发送字符串={}, length={}", client.getUser(), msg.length());
    }

    /**
     * @param msg 格式: [0]路径长度m, [1-(m+1)]api路径, [后面]入参数据
     */
    public void onMessage(byte[] msg, WsClient client) {
        if (!client.getHadConnected()) return;//doCloseSilent后, 状态已更新,但仍要拦截前端请求

        if (!client.getHadAuth()) {
            String apiPath = wsDataHandler.getApiPath(msg, client);
            if (!Objects.equals("wsLogin", apiPath)) {
                //未认证,但发送了其他消息; 被盗用
                log.warn("未认证={}, apiPath={}, length={}", client.getUser(), apiPath, msg.length);
                client.doCloseSilent();
                return;
            }
            //认证请求
            wsDataHandler.handleRequest(msg, client);
            return;
        }
        //其他业务请求
        wsDataHandler.handleRequest(msg, client);
    }

    /**
     * @param handlerMethod 前端处理消息的方法名,如'onKeyboardState'
     * @param data notNull
     */
    public void sendTo(WsClient client, String handlerMethod, Object data) {
        String toSend = WsDataParser.toWsData(handlerMethod, data);
        client.sendText(toSend);
    }

    public void sendToAll(String handlerMethod, Object data, WsClient from) {
        String toSend = WsDataParser.toWsData(handlerMethod, data);
        ArrayList<WsClient> copy = new ArrayList<>(clients);
        copy.forEach(target -> {
            if (target == from || !target.getHadConnected()) return;
            target.sendText(toSend);
        });
    }

}